﻿using System;
using Microsoft.Xna.Framework;
using Microsoft.Xna.Framework.Graphics;

namespace Asteroids.Graphics
{
    public class PrimitiveBatch : IDisposable
    {
        private const int BATCH_SIZE = 1024;

        private VertexPositionColor[] _batchBucket;
        private BasicEffect _effect;
        private GraphicsDevice _graphics;

        private bool _disposed;
        private bool _batchBegin;
        private int _batchPtr;
        private int _elementsPerPrimitive;
        private PrimitiveType _primitiveType;

        public PrimitiveBatch(GraphicsDevice graphics)
        {
            if (graphics == null)
                throw new ArgumentNullException("graphics", "GraphicsDevice cannot be null");

            _batchBucket = new VertexPositionColor[BATCH_SIZE];

            _effect = new BasicEffect(graphics);
            _effect.VertexColorEnabled = true;

            _batchBegin = false;
            _disposed = false;
            _batchPtr = 0;

            _graphics = graphics;
        }

        public void Dispose()
        { Dispose(false); }

        public void Dispose(bool disposing)
        {
            if (disposing)
            {
                if (!_disposed)
                    if (_effect != null)
                        _effect.Dispose();
            }
            _disposed = true;
        }

        ///<summary>
        /// Begin is called to tell the PrimitiveBatch what kind of primitives will be
        /// drawn, and to prepare the graphics card to render those primitives.
        /// </summary>
        public void Begin(PrimitiveType primType = PrimitiveType.LineList)
        {
            Viewport viewport = _graphics.Viewport;
            Begin(primType, Matrix.CreateOrthographicOffCenter(0, viewport.Width, viewport.Height, 0, 0, 1), Matrix.Identity);
        }

        ///<summary>
        /// Begin is called to tell the PrimitiveBatch what kind of primitives will be
        /// drawn, and to prepare the graphics card to render those primitives.
        /// </summary>
        public void Begin( PrimitiveType primType, Matrix projection, Matrix translation)
        {
            if (_batchBegin)
                throw new InvalidOperationException("Begin has already been called. Invoke End() before starting a new batch.");

            _primitiveType = primType;

            // how many verts will each of these primitives require?
            _elementsPerPrimitive = DeterminePrimitiveCount(primType);

            _effect.Projection = projection;
            _effect.World = translation;

            //tell our basic effect to begin.
            _effect.CurrentTechnique.Passes[0].Apply();

            // flip the error checking boolean. It's now ok to call AddVertex, Flush,
            // and End.
            _batchBegin = true;
        }

        ///<summary>
        /// AddVertex is called to add another vertex to be rendered. To draw a point,
        /// AddVertex must be called once. for lines, twice, and for triangles 3 times.
        /// this function can only be called once begin has been called.
        /// if there is not enough room in the vertices buffer, Flush is called
        /// automatically.
        /// </summary>
        public void AddVertex(Vector2 position, Color color, float zDepth = 0)
        {
            if (_batchBegin)
            {
                // are we starting a new primitive? if so, and there will not be enough room
                // for a whole primitive, flush.
                bool newPrimitive = ((_batchPtr % _elementsPerPrimitive) == 0);

                if (newPrimitive && (_batchPtr + _elementsPerPrimitive) >= _batchBucket.Length)
                    Flush();

                // once we know there's enough room, set the vertex in the buffer,
                // and increase position.
                _batchBucket[_batchPtr].Position = new Vector3(position, zDepth);
                _batchBucket[_batchPtr].Color = color;
                _batchPtr++;
            }
            else
                throw new InvalidOperationException("Begin must be called before AddVertex can be called.");
        }


        ///<summary>
        /// End is called once all the primitives have been drawn using AddVertex.
        /// it will call Flush to actually submit the draw call to the graphics card, and
        /// then tell the basic effect to end.
        /// </summary>
        public void End()
        {
            if (_batchBegin)
            {
                // Draw whatever the user wanted us to draw
                Flush();

                _batchBegin = false;
            }
            else
                throw new InvalidOperationException("Begin() must be invoked before End()");
        }

        ///<summary>
        /// Flush is called to issue the draw call to the graphics card. Once the draw
        /// call is made, positionInBuffer is reset, so that AddVertex can start over
        /// at the beginning. End will call this to draw the primitives that the user
        /// requested, and AddVertex will call this if there is not enough room in the
        /// buffer.
        /// </summary>
        private void Flush()
        {
            if (_batchBegin)
            {
                // if there is work to do
                if (_batchPtr != 0)
                {
                    // get the number of primitives to render
                    int primitivesToRender = _batchPtr / _elementsPerPrimitive;

                    // how many primitives will we draw?
                    int elementsLeftOver = _batchPtr % _elementsPerPrimitive;

                    // submit the draw call to the graphics card
                    _graphics.DrawUserPrimitives<VertexPositionColor>(_primitiveType, _batchBucket, 0, primitivesToRender);

                    // now that we've drawn, it's ok to reset positionInBuffer back to zero,
                    // and write over any vertices that may have been set previously.
                    _batchPtr = 0;
                }
            }
            else
                throw new InvalidOperationException("Begin must be called before flush can be called.");
        }

        private int DeterminePrimitiveCount(PrimitiveType primType)
        {
            int primitiveCount = 0;
            switch (primType)
            {
                case PrimitiveType.LineList:
                    primitiveCount = 2;
                    break;
                case PrimitiveType.TriangleList:
                    primitiveCount = 3;
                    break;
                case PrimitiveType.LineStrip:
                case PrimitiveType.TriangleStrip:
                default:
                    throw new ArgumentException("primitiveType", "This primitive type is not supported " + primType.ToString());
            }

            return primitiveCount;
        }
    }
}
